web 开发中,经常遇到需要实时刷新数据的场景,例如即时聊天、新闻推送、数据监控等。 目前常用的实现方式共 5 种:

# 1. 短轮询(Polling)

短轮询实现原理是,客户端设置定时器,每隔一定时间向服务端发送 http 请求,服务端收到请求后,不管是否有新数据,都进行响应,响应完成后即关闭本次 TCP 连接。客户端代码实现:

    setInterval(() => {
        fetch(url).then((res) => {
            // do something
        })
    }, 5000)

# 2. 长轮询(Long-Polling)

长轮询实现原理是,客户端发送 http 请求,服务端收到请求后,判断是否有新数据:

  • 若有,则进行响应,客户端收到响应后,渲染数据并发起新的请求;

  • 若无,则服务端 hold 连接,将请求挂起,请求处于 pending 状态,直到有数据或者超时才返回。

      refreshData() {
          fetch(url).then((res) => {
              // do something
              refreshData()
          }).catch((error) => {
              // do something
              refreshData()
          })
      }
    

# 3. 长连接(iframe)

长连接实现原理是,在页面中插入一个隐藏的 iframe 标签,利用 iframe 的 src 属性在客户端与服务端之间建立一个长连接,服务端向 iframe 传输数据(通常是<script>标签包裹的 js 代码,动态将有效数据插入页面或更新页面)。

    // 前端
    <div id="refresh_wrap"></div>
    <iframe src="/get-refresh-data" style="display: none;"></iframe>

    // 后端
    if (hasRefreshData) {
        res.write(`
            <script type="text/javascript">
                parent.document.getElementById('refresh_wrap').innerHTML = "${data}";  // 改变父窗口dom元素
            </script>
        `)
    }

# 4. 服务端推送事件(SSE, Server Sent Event)

严格来说,HTTP 协议是无法实现服务端推送的,但是当服务端向客户端声明接下来发送的是流信息时,连接将处于打开状态,SSE正是利用这一特性实现了服务端推送。
SSE在某写场景下可作为 WebSocket 的替代方案,二者对比如下:

  • SSE 是单向通道,仅能从服务端向客户端发送信息;

  • SSE 的实现成本更低,尤其是服务端;

  • SSE 不需要单独部署一套服务,而 WebSocket 需要单独部署;

      // 前端
      if (window.EventSource) {
          const source = new EventSource('/xxx/xxx')   // 创建 EventSource 对象并连接服务器
    
          // 连接成功后触发 open 事件
          source.addEventListener('open', () => {
              console.log('Connected');
          }, false);
    
          // 服务器发送信息到客户端时,如果没有 event 字段,默认会触发 message 事件
          source.addEventListener('message', e => {
              console.log(`data: ${e.data}`);
          }, false);
    
          // 自定义 EventHandler,在收到 event 字段为 slide 的消息时触发
          source.addEventListener('slide', e => {
              console.log(`data: ${e.data}`); // => data: 7
          }, false);
    
          // 连接异常时会触发 error 事件并自动重连
          source.addEventListener('error', e => {
              if (e.target.readyState === EventSource.CLOSED) {
              console.log('Disconnected');
              } else if (e.target.readyState === EventSource.CONNECTING) {
              console.log('Connecting...');
              }
          }, false);
      }
    
      // 服务端
      const http = require('http');
    
      http.createServer((req, res) => {
    
      // 服务器声明接下来发送的是事件流
      res.writeHead(200, {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive',
          'Access-Control-Allow-Origin': '*',
      });
    
      // 发送消息
      setInterval(() => {
          res.write('event: slide\n'); // 事件类型
          res.write(`id: ${+new Date()}\n`); // 消息 ID
          res.write('data: 7\n'); // 消息数据
          res.write('retry: 10000\n'); // 重连时间
          res.write('\n\n'); // 消息结束
      }, 3000);
    
      // 发送注释保持长连接
      setInterval(() => {
          res.write(': \n\n');
      }, 12000);
      }).listen(2000);
    

# 5. WebSocket

# 简介

http/https协议仅能由客户端发起请求不同,WebSocket既可以由客户端发起请求,也可以实现服务端主动向客户端推送信息,是一种真正的全双工通信协议。主要特点:

  • http/https一样,建立在TCP之上,服务端容易实现。
  • http/https兼容良好,默认端口也是80/443,因此握手阶段采用http/https实现连接。
  • 无同源限制,可与任何服务端通信,可取代 ajax。
  • 协议标识符为ws/wss(对应http/https),因此请求地址形如:ws://example.com:80/some/path

# 原理

如上所述,客户端发起WebSocket连接请求,其握手阶段采用http/https实现连接,但请求头与一般http/https有所不同,类似下面这样:

    GET / HTTP/1.1
    Connection: Upgrade
    Upgrade: websocket
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
  • Connection通知服务端,如果支持升级的话,进行协议升级。
  • Upgrade通知服务端,将协议由HTTP/1.1升级到WebSocket
  • Origin提供请求发出的域名,供服务端权限校验(服务端也可不校验)。
  • Sec-WebSocket-Key提供握手协议的密钥,是 Base64 编码的 16 字节随机字符串。

服务端的WebSocket响应类似如下:

    HTTP/1.1 101 Switching Protocols
    Connection: Upgrade
    Upgrade: websocket
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/
  • Connection通知客户端,如果支持升级的话,进行协议升级。
  • Upgrade通知客户端,将协议由HTTP/1.1升级到WebSocket
  • Sec-WebSocket-Accept提供握手协议的密钥,它是在Sec-WebSocket-Key后边拼接特定字符串后生成的字符串,以便客户端校验是否是目标服务端响应了请求。
  • Sec-WebSocket-Location提供进行通信的WebSocket地址。

完成握手以后,WebSocket协议就在 TCP 协议之上建立了连接,开始传送数据。

# 应用

客户端示例

客户端使用WebSocket无非进行如下操作:

  • 建立连接和断开连接
  • 发送数据和接收数据
  • 处理错误

代码示例:

    // 通过`http/https`握手,建立连接
    var ws = new WebSocket('wss://echo.websocket.org');

    // 连接成功后的回调
    ws.onopen = function(evt) {
        console.log('Connection open ...');
        // 向服务端发送数据
        ws.send('Hello WebSockets!');
    };

    // 收到服务端数据后的回调
    ws.onmessage = function(evt) {
        console.log('Received Message: ' + evt.data);
        // 断开连接
        ws.close();
    };

    // 连接断开后的回调
    ws.onclose = function(evt) {
        console.log('Connection closed.');
    };

    // 报错时的回调
    ws.onerror = function(event) {
        console.log('')
    };

服务端

WebSocket 协议需要服务器支持。常用的 Node 实现有以下三种:

  • µWebSockets
  • Socket.IO
  • WebSocket-Node
最后更新时间: 4/27/2021, 7:11:19 PM